package ch.ethz.inf.vs.sunspot.drivercore;


import ch.ethz.inf.vs.sunspot.Constants;

import com.sun.spot.io.j2me.radiogram.*;
import com.sun.spot.peripheral.TimeoutException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.microedition.io.*;

/**
 * This class handles synchronization of a single Sun SPOT. 
 * It is created and the synchronization loop is started as soon 
 * as the associated SPOT is encountered.
 * 
 * @author Simon Mayer, ETH Zurich
 *
 */
public class SpotSynchronizerThread implements Runnable {

	// Logging instance
	private static Logger logService = Logger.getLogger("SpotSynchronizerThread");

	private Thread t;
	private boolean hasStarted = false;
	private boolean keepRunning = true;
	int port = 0;
	String spotName = "";
	String spotAddress = "";

	/**
	 * Create a new SPOT synchronization instance for a Sun SPOT and start it
	 * @param spotPort the port the synchronization is to be performed at
	 * @param spotName the name of the Sun SPOT
	 * @param spotAddress
	 */
	public SpotSynchronizerThread(int spotPort, String spotName, String spotAddress) {
		logService.setLevel(Constants.LOGLEVEL);

		this.port = spotPort;
		this.spotName = spotName;
		this.spotAddress = spotAddress;
		this.keepRunning = true;

		t = new Thread(this, "Sun SPOT Synchronization @ " + spotPort);
		if(!this.hasStarted) t.start();
	}

	/**
	 * Stop this instance of the SPOT synchronization protocol
	 */
	public void stop() {
		logService.info("STOPPING SpotSynchronization...");

		hasStarted = false;
		keepRunning = false;
	}

	/**
	 * Start this instance of the SPOT synchronization protocol
	 */
	@Override
	public void run() {
		hasStarted = true;
		
		if (!spotName.startsWith(SpotRepresentation.USER_TOKEN)) logService.info("New Syncer has been spawned @ " + port + ", it handles spot " + spotName);
		else logService.info("User " + spotName.substring(SpotRepresentation.USER_TOKEN.length()) + " has logged in (port " + port + ")");

		RadiogramConnection recvConn = null;
		Datagram dg = null;

		String currentSpotName = "";
		String inFromSpot = "";
		String outToSpot = "";

		Boolean connOpen = false;

		// Open a connection to the spot
		while (!connOpen) {
			try {
				recvConn = (RadiogramConnection) Connector.open("radiogram://" + spotAddress + ":" + port);
				recvConn.setMaxBroadcastHops(0);
				recvConn.setTimeout(Constants.SPOT_SYNC_RECEIVETIMEOUT);
				connOpen = true;
			} catch (Exception e) {
				logService.severe("Connection still open - try again");
				e.printStackTrace();
				
				// Some precautions coming here...
				try {
					Thread.sleep(500);
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
				if (recvConn != null)
					try {
						recvConn.close();
					} catch (IOException e1) {
						e1.printStackTrace();
					}
			}
		}

		// Main synchronization loop
		do {
			try {
				// Create a new datagram and get its maximum length
				dg = recvConn.newDatagram(recvConn.getMaximumLength());
				dg.reset();

				// Read sensor sample received over the radio
				recvConn.receive(dg);
				inFromSpot = dg.readUTF();				

				// Get the name of the current spot
				currentSpotName = inFromSpot.substring(0, inFromSpot.indexOf(":"));
				logService.info("INFROMSPOT [" + spotName + "]: " + inFromSpot);

				// Re-prepare the datagram
				dg.reset();		

				// Check if the spot exists and if the current synchronizer is handling it
				if (SpotManagement.getInstance().spotExists(spotName) && currentSpotName.equalsIgnoreCase(spotName)) {
					// Get the time to re-synchronize with the spot (this can be adapted at runtime)
					Integer nextResyncTime = SpotManagement.getInstance().getFutureSyncTimeout(spotName);

					// Get JSON String to be transmitted to Sun SPOT (only the future of the SpotRepresentation)
					outToSpot = SpotManagement.getInstance().getSpotRepresentation(spotName).encodeObject_OutToSpot(nextResyncTime);									

					logService.info("OUTTOSPOT [" + spotName + "]: " + outToSpot);

					// log.info(Constants.MESSAGE_SPOT_RESYNC);
					dg.writeUTF(outToSpot);

					// After we have told the spot when to sync again, store it
					SpotManagement.getInstance().setSyncTimeout(spotName, nextResyncTime);
					recvConn.send(dg);

					// Decode JSON string received from the Sun SPOT and update its internal representation - a new Thread is used for that to keep overhead low
					new JSONHandlerThread(inFromSpot);

					// Reset the re-synchronization timer of this SPOT
					if (SpotManagement.getInstance().spotExists(spotName)) {
						SpotManagement.getInstance().totalSpots++;
						SpotManagement.getInstance().setTimeSinceLastSync(spotName, 0);
					}
					
					// If the SPOT has been removed from the Management side, log it
					else {
						logService.log(Level.INFO, "The spot " + spotName + " is trying to sync, but is not connected anymore...!");
					}
				}
			} catch (TimeoutException e) {										// Ignore TimeoutExceptions
			} catch (Exception e) {												// Catch any other Exceptions and close the connection, print the stack trace for debugging
				logService.severe("Exception in SpotSynchronization: " + e.getMessage());
				e.printStackTrace();

				// Close the connection
				try {
					keepRunning = false;					
					if (recvConn != null) recvConn.close();
				} catch (IOException e1) {
					logService.log(Level.SEVERE, e1.getCause() + " thrown in SpotDriverHeartBeat (lvl 1)");
				}
			}
		} while (keepRunning);

		// Make sure the connection is closed
		try {
			if (recvConn != null) {
				logService.fine("Closing recvConn");
				recvConn.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		System.out.println("<--[leave]- Sun SPOT " + spotName);
		
		if (spotName.startsWith(SpotRepresentation.USER_TOKEN)) logService.info("User " + spotName.substring(SpotRepresentation.USER_TOKEN.length()) + " has logged out!");
		logService.info("STOPPED SpotSynchronization!");
	}

	/**
	 * Thread that decodes incoming JSON messages
	 * @author mayersi
	 */
	private class JSONHandlerThread implements Runnable {
		Thread t;
		String inFromSpot;

		/**
		 * Create and start a new JSON handler thread
		 * @param inFromSpot, the JSON string to be decoded
		 */
		public JSONHandlerThread(String inFromSpot) {
			this.inFromSpot = inFromSpot;
			t = new Thread(this);
			t.run();
		}

		/**
		 * Start the JSON handler thread
		 */
		@Override
		public void run() {
			inFromSpot = inFromSpot.substring(inFromSpot.indexOf(":") + 1);

			if(SpotManagement.getInstance().spotExists(spotName)) { 
				SpotManagement.getInstance().getSpotRepresentation(spotName).decodeToObject_inFromSpot(inFromSpot);
			}
		}
	}
}